package parsetree;

import static com.google.common.base.Throwables.propagate;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.swing.event.DocumentEvent.ElementChange;
import javax.swing.text.AbstractDocument.ElementEdit;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;

import metagrammar.Operator;
import metagrammar.Type;
import parsetree.exceptions.DocumentContractBrokenException;

import com.google.common.collect.ImmutableList;

public class LineElement extends AbstractBranchElement implements Verifiable {
    
    public class NewlineElement extends AbstractLeafElement {
        
        public static final String NAME = "newline";
        
        public NewlineElement(BranchElement parent, int startOffset)
        throws BadLocationException
        {
            super(parent, startOffset);
        }
        
        @Override
        public int getEndOffset()
        {
            return super.getStartOffset() + 1;
        }
        
        @Override
        public Type getType()
        {
            // TODO Auto-generated method stub
            return Type.UNTYPED;
        }
        
        @Override
        public List<ElementChange> insertUpdate(Insert change, AttributeSet attributes)
        {
            // should never be called
            return null;
        }
        
        @Override
        public List<ElementChange> postRemoveUpdate(Removal change)
        {
            // should never be called
            return null;
        }
        
        @Override
        public Element merge(Element element)
        {
            // should never be called
            return null;
        }
        
        @Override
        public String getName()
        {
            return NAME;
        }
        
    }
    
    private class NewLinesAndChanges {
        public NewLinesAndChanges(List<LineElement> newLines, List<ElementChange> changes)
        {
            this.newLines = newLines;
            this.changes = changes;
        }
        private final List<LineElement> newLines;
        private final List<ElementChange> changes;
    }
    public static final String NAME = "line";
    
    public LineElement(BranchElement parent)
    {
        this(parent, new ArrayList<Element>(ParseTree.DEFAULT_CHILDREN));
    }
    
    private LineElement(BranchElement parent, ArrayList<Element> children)
    {
        super(parent, children);
    }
    
    private LineElement(BranchElement parent, ArrayList<Element> children, AttributeSet attributes)
    {
        super(parent, children, attributes);
    }
    
    public LineElement(BranchElement parent, AttributeSet attributes)
    {
        super(parent, new ArrayList<Element>(ParseTree.DEFAULT_CHILDREN), attributes);
    }
    
    @Override
    public Type getType()
    {
        if(!hasChildren()) {
            return Type.UNTYPED;
        }
        return getElement(getChildCount() - 1).getType();
    }
    
    @Override
    public String getName()
    {
        return NAME;
    }
    
    @Override
    public LineElement[] split(int modelOffset) throws BadLocationException
    {
        if(modelOffset <= getStartOffset()) {
            throw new BadLocationException(String.format(
                    "Model offset at %d is at or before the end of element to be split.",
                    modelOffset), modelOffset);
        }
        if(modelOffset >= getEndOffset()) {
            throw new BadLocationException(String.format(
                    "Model offset at %d is at or after the end of element to be split.",
                    modelOffset), modelOffset);
        }
        
        // Retrieves the index of the child that needs to be split, if it exists.
        int splitIndex = getIndexOfChildContaining(modelOffset);
        ImmutableList<Element> children = getChildren();
        ArrayList<Element> firstChildren;
        ArrayList<Element> secondChildren;
        if(splitIndex >= 0) {
            // First split will have the half of the children until the modelOffset
            firstChildren = new ArrayList<Element>(splitIndex + 1);
            firstChildren.addAll(children.subList(0, splitIndex));
            // First split will have the half of the children after the modelOffset
            secondChildren = new ArrayList<Element>(getChildCount() - splitIndex + 1);
            Element[] splitChild = getElement(splitIndex).split(modelOffset);
            firstChildren.add(splitChild[0]);
            secondChildren.add(splitChild[1]);
            secondChildren.addAll(children.subList(splitIndex + 1, getChildCount()));
        }
        else {
            // If no child exist at that modelOffset then we can simply split at the index.
            splitIndex = getIndexOfChildStartsWith(modelOffset);
            firstChildren = new ArrayList<Element>(children.subList(0, splitIndex));
            secondChildren = new ArrayList<Element>(children.subList(splitIndex, children.size()));
        }
        
        LineElement firstElement = new LineElement(getParent(), firstChildren, getAttributes());
        LineElement secondElement = new LineElement(getParent(), secondChildren, getAttributes());
        return new LineElement[] { firstElement, secondElement };
    }
    
    /**
     * @param change
     * @param attributes
     * @return
     * @throws BadLocationException
     */
    @Override
    public List<ElementChange> insertUpdate(Insert change, AttributeSet attributes)
    {
        // if there is an insert this LineElement may need to be split if a \n is present
        final int changesStart;
        final int changesEnd;
        String input;
        boolean addToBeginning = false;
        boolean onlyAddToBeginning = false;
        LinkedList<LineElement> newLines = new LinkedList<LineElement>();
        LinkedList<ElementChange> changes = new LinkedList<ElementChange>();
        if(change.getOffset() < getStartOffset()) {
            //if the change started before this element, there may be a newLine before this one
            addToBeginning = true;
            // we must look for newlines at the start of this element
            changesStart = getStartOffset();
        }
        else {
            // otherwise we can start looking at the beginning of the change
            changesStart = change.getOffset();
        }
        if((change.getOffset() + change.getLength()) < getEndOffset()) {
            changesEnd = change.getOffset() + change.getLength();
            if(changesEnd <= getStartOffset()) {
                //if changesEnd <= getStartOffset() this element cannot be changed
                // as a result of the DefaultDocumentEvent
                onlyAddToBeginning = true;
            }
        }
        else {
            changesEnd = getEndOffset();
        }
        if(addToBeginning) {
            try {
                if(onlyAddToBeginning) {
                    input = getDocument().getText(change.getOffset(), change.getLength());
                }
                else {
                    input = getDocument().getText(change.getOffset(),
                            changesStart - change.getOffset());
                }
                int lastIndex = 0;
                for(int index = 0; index > -1 && index < input.length();) {
                    index = input.indexOf('\n', index);
                    if(index >= 0) {
                        LineElement newLine = new LineElement(getParent(), getAttributes());
                        if(lastIndex < index) {
                            ElementEdit addition = newLine.addChild(new ParsingElement(newLine,
                                    change.getOffset() + lastIndex, change.getOffset() + index));
                            changes.add(addition);
                        }
                        ElementEdit newLineEddit = newLine.addChild(new NewlineElement(newLine,
                                change.getOffset() + index));
                        changes.add(newLineEddit);
                        newLines.add(newLine);
                        lastIndex = change.getOffset() + index;
                        index++;
                    }
                }
            }
            catch(BadLocationException e) {
                throw new DocumentContractBrokenException(e);
            }
        }
        // Having recovered the search indexes we now perform the actual search for the '\n's
        // that are inside the LineElement
        try {
            NewLinesAndChanges update = findNewLines(changesStart, changesEnd);
            newLines.addAll(update.newLines);
            if(update.changes != null) {
                changes.addAll(update.changes);
            }
        }
        catch(BadLocationException e) {
            throw new DocumentContractBrokenException(e);
        }
        if(newLines.size() > 0) {
            // if new lines were created they are added to the parent  and the change is propagated down to their children.
            LineElement[] newLineAr = newLines.toArray(new LineElement[0]);
            changes.add(getParent().addChildren(newLineAr, false, false));
            for(LineElement line : newLines) {
                List<ElementChange> childUpdate = line.insertUpdateChildren(change, attributes);
                if(childUpdate != null) {
                    changes.addAll(childUpdate);
                }
            }
            return changes;
        }
        else {
            return insertUpdateChildren(change, attributes);
        }
    }
    
    /**
     * @param changesStart
     * @param changesEnd
     * @param currentLine
     * @param newLines
     * @return
     * @throws BadLocationException
     */
    private NewLinesAndChanges findNewLines(final int changesStart, final int changesEnd)
    throws BadLocationException
    {
        LinkedList<LineElement> newLines = new LinkedList<LineElement>();
        LinkedList<ElementChange> changes = null;
        
        String input = getDocument().getText(changesStart, changesEnd - changesStart);
        
        LineElement currentLine = this;
        int lastOffset = 0;
        for(int index = 0; index > -1 && index < input.length();) {
            index = input.indexOf('\n', index);
            if(index >= 0) {
                // If a '\n' is found a NewLineElement needs to be added
                // and the latest LineElement needs to be split after it.
                NewlineElement newLineElement = new NewlineElement(currentLine, changesStart
                        + index);
                currentLine.addChild(newLineElement);
                LineElement[] splitArray = currentLine.split(newLineElement.getEndOffset());
                
                if(splitArray.length > 0 && lastOffset < splitArray[0].getStartOffset()) {
                    // if there is a gap between the end of the last LineElment
                    // and the first of the new ones we add an Element to fill the gap
                    if(changes == null) {
                        changes = new LinkedList<ElementChange>();
                    }
                    ParsingElement gapFiller = new ParsingElement(splitArray[0], lastOffset, index);
                    changes.add(splitArray[0].addChild(gapFiller));
                }
                
                for(int i = 0; i < splitArray.length - 1; i++) {
                    newLines.add(splitArray[i]);
                }
                currentLine = splitArray[splitArray.length - 1];
                lastOffset = currentLine.getEndOffset();
                index++;
            }
        }
        
        if(currentLine != this || newLines.size() > 0) {
            newLines.add(currentLine);
        }
        
        return new NewLinesAndChanges(newLines, changes);
    }
    
    /**
     * @param change
     * @param attributes
     * @return
     */
    @Override
    public List<ElementChange> insertUpdateChildren(Insert change, AttributeSet attributes)
    {
        int offset = change.getOffset();
        Element toChange = getChildContaining(offset);
        List<ElementChange> changes = null;
        if(toChange != null) {
            changes = toChange.insertUpdate(change, attributes);
        }
        else {
            toChange = getChildStartsWith(offset);
            if(toChange != null) {
                changes = toChange.insertUpdate(change, attributes);
            }
            else {
                int index = getParent().getIndexOfChildEndsWith(offset);
                if((index >= 0 && index < getParent().getChildCount() - 1 && getParent()
                        .getElement(index) == this)
                        || (index < 0 && getParent().getElement( -(index + 1)) == this)) {
                    // if none of the children were changed and the change is still contained
                    // within this Element, the insert must have created a gap, so we create
                    // a new Element to interpret the added text.
                    try {
                        changes = new LinkedList<ElementChange>();
                        ParsingElement newLeaf = new ParsingElement(this, change.getOffset(),
                                change.getOffset() + change.getLength());
                        changes.add(addChild(newLeaf));
                        changes.addAll(newLeaf.insertUpdate(change, attributes));
                    }
                    catch(BadLocationException e) {
                        propagate(e);
                    }
                }
            }
            if(changes == null || changes.isEmpty()) {
                toChange = getChildStartsWith(offset + change.getLength());
                if(toChange != null) {
                    changes = toChange.insertUpdate(change, attributes);
                }
            }
        }
        return changes;
    }
    
    @Override
    public List<ElementChange> postRemoveUpdate(Removal change)
    {
        List<ElementChange> changes = new LinkedList<ElementChange>();
        if(change.getOffset() == getEndOffset()) {
            Element nextSibling = getParent().getChildStartsWith(getEndOffset());
            LineElement combinedLine = merge(nextSibling);
            if(combinedLine != null) {
                changes.add(getParent().addChild(combinedLine));
            }
        }
        
        List<ElementChange> childChange = postRemoveUpdateChildren(change);
        if(childChange != null) {
            changes.addAll(childChange);
        }

        return changes;
    }
    
    @Override
    public LineElement merge(Element element)
    {
        if(element instanceof LineElement && getParent().equals(element.getParent())) {
            if(getEndOffset() == element.getStartOffset()) {
                return handleMerge(this, (LineElement) element);
            }
            else if(getStartOffset() == element.getEndOffset()) {
                return handleMerge((LineElement) element, this);
            }
        }
        return null;
    }
    
    private LineElement handleMerge(LineElement firstLine, LineElement secondLine)
    {
        try {
            if(firstLine.getStartOffset() == firstLine.getEndOffset()
                    || !getDocument().getText(firstLine.getStartOffset(), 1).equals("\n")) {
                //only merge if the first line doesn't end with \n
                ImmutableList<Element> firstChildren = firstLine.getChildren();
                ImmutableList<Element> secondChildren = secondLine.getChildren();
                int childrenSize = Math.max(
                        firstLine.getChildCount() + secondLine.getChildCount(),
                        ParseTree.DEFAULT_CHILDREN);
                ArrayList<Element> mergedChildren = new ArrayList<Element>(childrenSize);
                if(firstLine.hasChildren()) {
                    // if the first line has children may need to merge children
                    for(int i = 0; i < firstChildren.size() - 1; i++) {
                        mergedChildren.add(firstChildren.get(i));
                    }
                    if(secondLine.hasChildren()) {
                        // if the second line has children may need to merge children
                        Element firstBorderChild = firstChildren.get(firstChildren.size() - 1);
                        Element secondBorderChild = secondChildren.get(0);
                        // attempt a merge
                        Element merge = firstBorderChild.merge(secondBorderChild);
                        if(merge != null) {
                            // if the merge is successful add the merged child
                            mergedChildren.add(merge);
                        }
                        else {
                            // otherwise add the unmerged children
                            mergedChildren.add(firstBorderChild);
                            mergedChildren.add(secondBorderChild);
                        }
                        for(int i = 1; i < secondChildren.size(); i++) {
                            mergedChildren.add(secondChildren.get(i));
                        }
                    }
                }
                else {
                    mergedChildren.addAll(secondChildren);
                }
                return new LineElement(firstLine.getParent(), mergedChildren, getAttributes());
            }
        }
        catch(BadLocationException e) {
            throw new DocumentContractBrokenException(e);
        }
        return null;
    }
    
    @Override
    public boolean verify()
    {
        int length = getChildCount();
        for(int i = 0; i < length;) {
            Element child = getElement(i);
            if(child.getType().equals(ParseTree.equivType)) {
                if((i - 1) >= 0 && (i + 1) < length) {
                    Element previousChild = getElement(i - 1);
                    Element nextChild = getElement(i + 1);
                    i += 2;
                    if(previousChild instanceof OperatorElement
                            && nextChild instanceof OperatorElement) {
                        if(((Verifiable) nextChild).verify()) {
                            Operator.Instance prevOperator = ((DefaultOperatorElement) previousChild)
                            .getOperatorInstance();
                            Operator.Instance nextOperator = ((DefaultOperatorElement) nextChild)
                            .getOperatorInstance();
                            if(!prevOperator.isEquivalentTo(nextOperator)) {
                                return false;
                            }
                        }
                        else {
                            return false;
                        }
                    }
                    else {
                        return false;
                    }
                }
                else {
                    return false;
                }
            }
            else {
                i++;
            }
            if(child instanceof Verifiable) {
                if(!((Verifiable) child).verify()) {
                    return false;
                }
            }
        }
        return true;
    }
}